import { CSSProperties, useEffect, useRef, useState } from "react";
import useLatestRef from "./useLatestRef";

type Offset = {
  top: number;
  left: number;
};

type DragableOptions<T extends HTMLElement> = {
  mode?: "drag" | "click";
  enable?: boolean;
  initialOffset?: Offset | (() => Offset);
  forceViewport?: boolean;
  onDragStart?: (
    el: T,
    offset: Offset,
    updateOffset: (offset: Offset) => void,
  ) => void;
  onDrag?: (
    el: T,
    offset: Offset,
    updateOffset: (offset: Offset) => void,
  ) => void;
  onDragEnd?: (
    el: T,
    offset: Offset,
    updateOffset: (offset: Offset) => void,
  ) => void;
};

export default function useDragable<
  T extends HTMLElement = HTMLElement,
  U extends HTMLElement = HTMLElement,
>(options?: DragableOptions<T>) {
  const optionsRef = useLatestRef(options);
  const { initialOffset = { top: 0, left: 0 } } = optionsRef.current || {};
  const elRef = useRef<T>(null);
  const forceElementRef = useRef<U>(null);
  const [isDragging, setIsDragging] = useState(false);
  const [offset, setOffset] = useState(() => {
    const t =
      typeof initialOffset === "function" ? initialOffset() : initialOffset;
    return t;
  });
  const startPositionRef = useRef<{
    x: number;
    y: number;
    originX: number;
    originY: number;
  }>();
  const startBoundingRef = useRef<DOMRect>();
  const offsetRef = useLatestRef(offset);

  useEffect(() => {
    if (!elRef.current) {
      return;
    }
    const isDragMode = options?.mode === "drag";
    const el = elRef.current;
    if (isDragMode) {
      el.setAttribute("dragable", "true");
    }

    function onDragstart(e: DragEvent | MouseEvent) {
      if (optionsRef.current?.enable === false) {
        return;
      }
      setIsDragging(true);
      const el = e.target as HTMLElement;
      if (e instanceof DragEvent) {
        e.dataTransfer?.setDragImage(el, window.outerWidth, window.outerHeight);
      } else {
        document.addEventListener("mousemove", onDrag);
        document.addEventListener("mouseup", onDragend);
      }

      startPositionRef.current = {
        x: e.clientX,
        y: e.clientY,
        originX: offsetRef.current.left,
        originY: offsetRef.current.top,
      };
      if (optionsRef.current?.forceViewport) {
        startBoundingRef.current = (
          forceElementRef.current || el
        ).getBoundingClientRect();
      }
    }

    function onDrag(e: DragEvent | MouseEvent) {
      if (optionsRef.current?.enable === false) {
        return;
      }
      const { clientX, clientY } = e;
      if (e instanceof DragEvent && clientX === 0 && clientY === 0) {
        // 松开 drag 的前一个事件坐标突然变为 0
        return;
      }
      const start = startPositionRef.current;
      if (!start) {
        throw new Error("drag is not start");
      }
      let dx = e.clientX - start.x;
      let dy = e.clientY - start.y;
      if (optionsRef.current?.forceViewport) {
        const { top, left, bottom, right, height, width } =
          startBoundingRef.current!;
        if (left + dx < 0) {
          dx -= left + dx;
        } else if (right + dx > window.innerWidth) {
          dx -= right + dx - window.innerWidth;
        }
        if (top + dy < 0) {
          dy -= top + dy;
        } else if (bottom + dy > window.innerHeight) {
          dy -= bottom + dy - window.innerHeight;
        }
      }
      const newX = start.originX + dx;
      const newY = start.originY + dy;
      setOffset({ left: newX, top: newY });
    }

    function onDragend(e: DragEvent | MouseEvent) {
      if (optionsRef.current?.enable === false) {
        return;
      }
      setIsDragging(false);
      optionsRef.current?.onDragEnd?.(el, offsetRef.current, setOffset);
      if (e instanceof MouseEvent) {
        document.removeEventListener("mousemove", onDrag);
        document.removeEventListener("mouseup", onDragend);
      }
      startPositionRef.current = undefined;
      if (optionsRef.current?.forceViewport) {
        startBoundingRef.current = undefined;
      }
    }

    if (isDragMode) {
      el.addEventListener("dragstart", onDragstart);
      el.addEventListener("drag", onDrag);
      el.addEventListener("dragend", onDragend);
    } else {
      el.addEventListener("mousedown", onDragstart);
    }

    return () => {
      if (isDragMode) {
        el.removeEventListener("dragstart", onDragstart);
        el.removeEventListener("drag", onDrag);
        el.removeEventListener("dragend", onDragend);
      } else {
        el.removeEventListener("mousedown", onDragstart);
      }
    };
  }, [options?.mode]); // eslint-disable-line

  return {
    ref: elRef,
    forceElementRef,
    offset,
    isDragging,
  };
}
